package com.zhao.dota.service.impl;

import cn.hutool.core.io.FileUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.zhao.dota.common.ParseWatch;
import com.zhao.dota.contant.CacheConstant;
import com.zhao.dota.contant.GameModelConst;
import com.zhao.dota.contant.TeamConst;
import com.zhao.dota.exception.MatchException;
import com.zhao.dota.model.*;
import com.zhao.dota.parser.DetailParser;
import com.zhao.dota.parser.PerformanceParser;
import com.zhao.dota.processor.MatchProcessor;
import com.zhao.dota.processor.QiniuProcessor;
import com.zhao.dota.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import skadistats.clarity.Clarity;
import skadistats.clarity.source.MappedFileSource;
import skadistats.clarity.wire.common.proto.Demo;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.time.YearMonth;
import java.time.ZoneOffset;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
@Slf4j
public class ImportMatchServiceImpl implements ImportMatchService {

    @Resource
    private MatchService matchService;
    @Resource
    private BanPickService pickService;
    @Resource
    private MatchPlayerService matchPlayerService;
    @Resource
    private MatchPlayerItemService matchPlayerItemService;
    @Resource
    private PerformanceService performanceService;
    @Resource
    private MatchPlayerAbilityService matchPlayerAbilityService;
    @Resource
    private QiniuProcessor qiniuProcessor;
    @Resource
    private MatchProcessor matchProcessor;
    @Value("${server.tomcat.basedir}")
    private String tempPath;
    @Resource
    private ReportService reportService;
    @Lazy
    @Autowired
    private ImportMatchService importMatchService;
    @Resource
    private PlayerProcessService playerProcessService;

    private final Cache cache;

    private final Object lock = new Object();

    @Autowired
    public ImportMatchServiceImpl(CacheManager cacheManager) {
        this.cache = cacheManager.getCache(CacheConstant.MATCH);
    }

    @PostConstruct
    public void init() {
        new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    try {
                        List<ImportMatch> importMatches = waitList();
                        if (importMatches.isEmpty()) {
                            lock.wait(60 * 1000);
                            importMatches = waitList();
                        }
                        if (!importMatches.isEmpty()) {
                            ImportMatch importMatch = importMatches.get(0);
                            importMatchService.doParse(importMatch);
                        }
                    } catch (Exception e) {
                        log.error("", e);
                    }
                }
            }
        }, "录像解析线程").start();
    }

    private List<ImportMatch> waitList() {
        synchronized (lock) {
            List<ImportMatch> waitList = cache.get("waitList", List.class);
            if (waitList == null) {
                return new ArrayList<>();
            }
            return waitList;
        }
    }

    private void addWait(List<Long> ids, List<String> paths) {
        synchronized (lock) {
            List<ImportMatch> list = waitList();
            if (paths == null) {
                paths = new ArrayList<>();
                for (int i = 0; i < ids.size(); i++) {
                    paths.add("");
                }
            }
            for (int i = 0; i < ids.size(); i++) {
                list.add(new ImportMatch(paths.get(i), ids.get(i)));
            }
            cache.put("waitList", list);
            lock.notify();
        }
    }
    private void addWait(long id, String path) {
        addWait(Collections.singletonList(id), Collections.singletonList(path));
    }

    private boolean exists(long id) {
        List<ImportMatch> importMatches = waitList();
        for (ImportMatch importMatch : importMatches) {
            if (importMatch.getMatchId() == id) {
                return true;
            }
        }
        return false;
    }

    public void delWait(long id) {
        List<ImportMatch> list = waitList();
        for (Iterator<ImportMatch> iterator = list.iterator(); iterator.hasNext();) {
            ImportMatch next = iterator.next();
            if (next.getMatchId() == id) {
                iterator.remove();
                break;
            }
        }
        cache.put("waitList", list);
    }

    @Override
    public void importMatch(File matchFile) throws IOException {
        Demo.CDemoFileInfo matchInfo = parseInfo(matchFile);
        long matchId = matchInfo.getGameInfo().getDota().getMatchId();
        boolean practice = matchProcessor.isPractice(matchId);
        if (!practice) {
            throw new MatchException("比赛[%s]不是练习赛，目前不支持导入");
        }
        DotaMatch db = matchService.getByMatchId(matchId);
        if (db == null) {
            boolean flag = saveToQiniu(matchFile, matchId);
            Assert.isTrue(flag, "上传录像到七牛云失败，matchId：" + matchId);
        }
        if (exists(matchId)) {
            return;
        }
        addWait(matchId, matchFile.getPath());
    }

    @Override
    public void importMatch(Long matchId) {
        boolean practice = matchProcessor.isPractice(matchId);
        if (!practice) {
            throw new MatchException("比赛[%s]不是练习赛，目前不支持导入");
        }
        if (exists(matchId)) {
            return;
        }
        addWait(matchId, null);
    }

    @Override
    public void parseReply(List<Long> ids) {
        for (int i = 0; i < ids.size();) {
            if (exists(i)) {
                ids.remove(i);
            } else {
                i++;
            }
        }
        if (!ids.isEmpty()) {
            addWait(ids, null);
        }
    }

    private Demo.CDemoFileInfo parseInfo(File matchFile) throws IOException {
        return Clarity.infoForSource(new MappedFileSource(matchFile));
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void doParse(ImportMatch importMatch) {
        ParseWatch watch = new ParseWatch(importMatch.getMatchId());
        try {
            File matchFile;
            if (!StringUtils.hasText(importMatch.getFilePath())) {
                watch.start("下载录像文件");
                matchFile = downloadFromQiniu(importMatch);
            } else {
                matchFile = new File(importMatch.getFilePath());
                if (!matchFile.exists()) {
                    matchFile = downloadFromQiniu(importMatch);
                }
            }
            watch.start("parse info");
            Demo.CDemoFileInfo info = parseInfo(matchFile);
            Demo.CGameInfo gameInfo = info.getGameInfo();
            Demo.CGameInfo.CDotaGameInfo dota = gameInfo.getDota();

            DotaMatch dotaMatch = matchService.getByMatchId(dota.getMatchId());
            if (dotaMatch == null) {
                dotaMatch = new DotaMatch();
            }
            dotaMatch.setEndTime(new Date(1000L * dota.getEndTime()).toInstant().atOffset(ZoneOffset.of("+8")).toLocalDateTime());
            dotaMatch.setWinner(TeamConst.ofCode(dota.getGameWinner()));
            dotaMatch.setMatchId(dota.getMatchId());
            dotaMatch.setGameMode(GameModelConst.ofIndex(dota.getGameMode()));
            dotaMatch.setPlaybackTime((double) info.getPlaybackTime());
            matchService.saveOrUpdate(dotaMatch);

            List<Demo.CGameInfo.CDotaGameInfo.CHeroSelectEvent> picksBansList = dota.getPicksBansList();
            List<BanPick> banPickList = new ArrayList<>();
            for (int i = 0; i < picksBansList.size(); i++) {
                Demo.CGameInfo.CDotaGameInfo.CHeroSelectEvent pickBan = picksBansList.get(i);
                BanPick banPick = new BanPick();
                banPick.setHeroId(pickBan.getHeroId());
                banPick.setPick(pickBan.getIsPick());
                banPick.setMatchId(dotaMatch.getId());
                banPick.setTeam(TeamConst.ofCode(pickBan.getTeam()));
                banPick.setOrderNum(i);
                banPickList.add(banPick);
            }
            pickService.saveList(banPickList);

            List<Demo.CGameInfo.CDotaGameInfo.CPlayerInfo> playerInfoList = dota.getPlayerInfoList();
            List<MatchPlayer> playerList = new ArrayList<>();
            for (Demo.CGameInfo.CDotaGameInfo.CPlayerInfo value : playerInfoList) {
                MatchPlayer matchPlayer = new MatchPlayer();
                matchPlayer.setMatchId(dotaMatch.getId());
                matchPlayer.setName(value.getPlayerName());
                matchPlayer.setHeroName(value.getHeroName());
                matchPlayer.setSteamId(value.getSteamid());
                matchPlayer.setTeam(TeamConst.ofCode(value.getGameTeam()));
                if (matchPlayer.getTeam() == dotaMatch.getWinner()) {
                    matchPlayer.setWin(1);
                } else {
                    matchPlayer.setWin(0);
                }
                matchPlayer.setEndTime(dotaMatch.getEndTime());
                playerList.add(matchPlayer);
            }
            Map<String, MatchPlayer> playerMap = playerList.stream().collect(Collectors.toMap(MatchPlayer::getHeroName, Function.identity()));
            Map<Long, MatchPlayer> steamIdPlayerMap = playerList.stream().collect(Collectors.toMap(MatchPlayer::getSteamId, Function.identity()));
            matchPlayerService.saveList(dotaMatch, playerList);

            watch.start("parse detail");
            DetailParser detailParser = new DetailParser(matchFile);
            detailParser.parse();
            watch.end();

            dotaMatch.setGameTime(detailParser.getGameTime());

            // 英雄装备
            List<MatchPlayerItem> items = new ArrayList<>();
            Map<String, List<MatchPlayerItem>> map = detailParser.getItemMap();
            for (Map.Entry<String, List<MatchPlayerItem>> entry : map.entrySet()) {
                String heroName = entry.getKey();
                List<MatchPlayerItem> itemList = entry.getValue();
                MatchPlayer matchPlayer = playerMap.get(heroName);
                for (MatchPlayerItem item : itemList) {
                    item.setLocalizedName(null);
                    item.setMatchPlayerId(matchPlayer.getId());
                    item.setSteamId(matchPlayer.getSteamId());
                    item.setMatchId(dotaMatch.getId());
                    items.add(item);
                }
            }
            matchPlayerItemService.saveList(items);
            List<PlayerProcess> processList = new ArrayList<>();
            // 每分钟英雄金钱/经验
            Map<Integer, List<PlayerProcess>> playerProcessMap = detailParser.getPlayerProcessMap();
            for (Map.Entry<Integer, List<PlayerProcess>> entry : playerProcessMap.entrySet()) {
                Integer time = entry.getKey();
                if (time == -1) {
                    time = detailParser.getGameTime().intValue();
                }
                List<PlayerProcess> list = entry.getValue();
                for (PlayerProcess playerProcess : list) {
                    playerProcess.setTime(time);
                    MatchPlayer matchPlayer = steamIdPlayerMap.get(playerProcess.getSteamId());
                    playerProcess.setPlayerId(matchPlayer.getPlayerId());
                    playerProcess.setMatchPlayerId(matchPlayer.getId());
                    playerProcess.setMatchId(dotaMatch.getId());
                    processList.add(playerProcess);
                }
            }
            playerProcessService.saveList(dotaMatch.getId(), processList);
            // 技能
            Map<String, List<MatchPlayerAbility>> abilityMap = detailParser.getAbilityMap();
            matchPlayerAbilityService.saveList(abilityMap, dotaMatch.getId(), playerMap);

            Map<String, Integer> towerDamageMap = detailParser.getTowerDamageMap();
            watch.start("parse performance");
            PerformanceParser performanceParser = new PerformanceParser(matchFile);
            try {
                performanceParser.parse();
            } catch (InterruptedException ignore) {
            } finally {
                watch.end();
            }
            List<Performance> performances = performanceParser.getPerformances();
            int direKills = 0;
            int radiantKills = 0;
            double minutes = dotaMatch.getGameTime() / 60;
            for (Performance performance : performances) {
                MatchPlayer matchPlayer = steamIdPlayerMap.get(performance.getSteamId());
                performance.setMatchId(dotaMatch.getId());
                performance.setMatchPlayerId(matchPlayer.getId());
                performance.setTowerDamage(towerDamageMap.getOrDefault(matchPlayer.getHeroName(), 0));
                if (matchPlayer.getTeam() == TeamConst.DIRE) {
                    direKills += performance.getKills();
                } else {
                    radiantKills += performance.getKills();
                }
                performance.setEndTime(dotaMatch.getEndTime());
                performance.setGpm(performance.getTotalEarnedGold() / minutes);
                performance.setXpm(performance.getTotalEarnedXp() / minutes);
            }
            dotaMatch.setDireKills(direKills);
            dotaMatch.setRadiantKills(radiantKills);
            matchService.updateById(dotaMatch);

            performanceService.saveList(performances);
            parseSuccess(dotaMatch);
        } catch (Exception e) {
            log.error("录像解析失败， match id: " + importMatch.getMatchId(), e);
        } finally {
            watch.finish();
            delWait(importMatch.getMatchId());
        }
    }

    private void parseSuccess(DotaMatch dotaMatch) {
        YearMonth yearMonth = YearMonth.from(dotaMatch.getEndTime());
        reportService.clearCache(yearMonth);
    }

    private boolean saveToQiniu(File file, Long matchId) {
        String fileName = matchId + ".dem";
        if (!qiniuProcessor.replyExists(fileName)) {
            return qiniuProcessor.uploadReply(file, fileName);
        }
        return true;
    }

    private File downloadFromQiniu(ImportMatch importMatch) {
        String name = importMatch.getMatchId() + ".dem";
        File file = new File(tempPath + "/" + name);
        if (!file.exists()) {
            byte[] bytes = qiniuProcessor.downloadReply(name);
            FileUtil.writeBytes(bytes, file);
        }
        return file;
    }

}
